這是 JavaScript 惡名昭彰的東西之一,閉包 ( closure )。
Tony 將會跟你說要抓到最重要的兩個重點就好!
這邊請當故事看,說明閉包真的需要花時間來了解,才會有剎那間得到的瞬間。是一個不斷挖金礦的過程。
閉包不是必修學科,也不是威猛武器。
但閉包到處都是,一開始 Tony 還認不出來。現在想辦法來找到威利吧。
closure 是函式記得並存取其語彙範圍的能力,甚至當函式是在其語彙範圍外執行時,也是如此。
function foo(){
var a = 2;
function bar(){
console.log( a ); //2
}
bar(); // 在語彙範圍外不能執行
}
foo();
bar() 能夠存取 a,是因為語彙範疇的 RHS 查找規則。這或許算是閉包,但如果照定義看就不完全是。因為不能在語彙範疇外執行。
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2
兩個重點
開始執行
function foo() {
var a = 2;
function baz() {
console.log( a ); // 2
}
bar(baz);
}
function bar(fn) {
fn();
}
執行 foo() 可以得到想要的結果,但不算嚴格的閉包。因為 foo 的環境不會消失。
var fn;
function foo() {
var a = 2;
function baz() {
console.log( a );
}
fn = baz;
}
function bar() {
fn();
}
foo();
bar(); // 2
一樣兩個重點
這邊的意思是,只要能夠保留著
下面丟出了共三個範例
function wait( message ){
setTimeout( function timer(){
console.log( message );
},1000);
}
wait( "Hello, closure!" );
function setupBot(name, selector){
$( selector ).click( funtion activator(){
console.log("Activating: " + name);
});
}
setupBot( "closure Bot 1", "#bot_1" );
setupBot( "closure Bot 2", "#bot_2" );
以上這兩個範例,泛指只要把函數當成一級 ( first-class ) 的值傳遞 ( Functional Programming ),就是用到閉包。
(這邊有點想不透,因為他們都還是直接呼叫函式本身,所處的環境並沒有被回收。)
var a = 2;
(funtion IIFE(){
console.log( a );
}());
a 是藉由正常的語彙範疇查找成功,並非真的透過 closure。所以不算是使用了 closure,就算 IIFE 和 closure 關係緊密。
( Tony 有點搞糊塗了,因為 global 也和前面兩個一樣沒有被回收,但就不算是閉包?)
下面迴圈使用結果,預期是每過一秒印出 1, 2,.., 5。
for (var i = 1; i < 5; i++) {
setTimeout( function timer(){
console.log( i );
}, i*1000 );
}
但實際上是 6, 6, 6, 6, 6。
咦?
這邊有兩個問題
ans1: 因為 for 迴圈,在處理的時候,會先判定是否 i <= 5
。只有 i > 5
時會跳出迴圈,也就是 6。
ans2:
首先,這就會意識到
就用 IIFE 吧!既可以切割範疇,又可以當下執行。
for (var i = 1; i <= 5; i++ ) {
(function() {
setTimeout( function timer(){
console.log( i );
},i * 1000);
})();
}
這個結果是不行的。
因為我們的確切割出範疇,但還是要存下值才有用。不然他還是空的範疇。
for (var i = 1; i < 5; i++ ) {
(function() {
var j = i;
setTimeout( function timer(){
console.log( j );
},j * 1000);
})();
}
這時候才有切割好的範疇,和不同的值。
稍微變化的寫法。運用隱含的 LHS。
for (var i = 1; i < 5; i++ ) {
(function(j) {
setTimeout( function timer(){
console.log( j );
},j * 1000);
})(i);
}
另一個更棒的方法。
let 會劫持區塊,並在區塊宣告變數。( 剛好都有滿足! )
for ( var i = 1; i <= 5; i++) {
let j = i;
setTimeout( function timer() {
console.log(j);
}, j * 1000 );
}
最後就變成
for ( let i = 1; i <= 5; i ++) {
setTimeout( function timer() {
console.log(i);
}, i * 1000 );
}
其實這是在每次的迭代都宣告一次。而且他是用前一次迭代最後得值。這是 let 在 for 迴圈標頭 ( header ) 的特殊行為。
最後的結果就變成,只要把 var 換成 let 就可以了。
那讓我成為了一名快樂的 JavaScript 編程員 ( JavaScripter )。